package org.geoscrape;

import static org.geoscrape.util.HtmlParser.getContent;

import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;

import org.geoscrape.util.CacheEstimator;
import org.geoscrape.util.ImageToolbox;
import org.geoscrape.util.MaskSet;
import org.geoscrape.util.MaskSet.MaskEntry;
import org.geoscrape.util.WebClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

/**
 * 
 * Object that searches for caches using the map.
 * 
 */
public class MapSearcher extends Searcher
{
	private String sessionToken;
	private String magic;
	private String subscriberType;
	private final int EDGE_THICKNESS_BIG = 7;
	private final int EDGE_THICKNESS_MID = 7;
	private final int EDGE_THICKNESS_SML = 3;
	private ImageToolbox imageToolbox;

	public MapSearcher(Login login)
	{
		super(login);

		MaskSet maskset = new MaskSet();
		imageToolbox = new ImageToolbox(maskset);
		try
		{
			maskset.setBigIconBorder(ImageIO
					.read(new File("templates/largeiconframe.png")));
			maskset.setSmallIconBorder(ImageIO.read(new File(
					"templates/smalliconframe.png")));
			maskset.setLargeTrad(ImageIO.read(new File("templates/large/trad.png")));
			maskset.setLargeApe(ImageIO.read(new File("templates/large/ape.png")));
			maskset.setLargeLetter(ImageIO.read(new File("templates/large/letter.png")));
			maskset.setLargeMulti(ImageIO.read(new File("templates/large/multi.png")));
			maskset.setLargeEvent(ImageIO.read(new File("templates/large/event.png")));
			maskset.setLargeMega(ImageIO.read(new File("templates/large/megaevent.png")));
			maskset.setLargeCito(ImageIO.read(new File("templates/large/cito.png")));
			maskset.setLargeGpsAdv(ImageIO
					.read(new File("templates/large/adventure.png")));
			maskset.setLargeVirtual(ImageIO.read(new File("templates/large/virtual.png")));
			maskset.setLargeWebcam(ImageIO.read(new File("templates/large/webcam.png")));
			maskset.setLargeEarth(ImageIO.read(new File("templates/large/earth.png")));
			maskset.setLargeMyst(ImageIO.read(new File("templates/large/myst.png")));
			maskset.setLargeWherigo(ImageIO.read(new File("templates/large/wherigo.png")));
			maskset.setLargeFound(ImageIO.read(new File("templates/large/found.png")));
			maskset.setLargeOwn(ImageIO.read(new File("templates/large/own.png")));

			maskset.setMediumEvent(ImageIO.read(new File("templates/medium/event.png")));
			maskset.setMediumFound(ImageIO.read(new File("templates/medium/found.png")));
			maskset.setMediumMulti(ImageIO.read(new File("templates/medium/multi.png")));
			maskset.setMediumMyst(ImageIO.read(new File("templates/medium/myst.png")));
			maskset.setMediumOwn(ImageIO.read(new File("templates/medium/own.png")));
			maskset.setMediumTrad(ImageIO.read(new File("templates/medium/trad.png")));
			maskset.setMediumVirtual(ImageIO
					.read(new File("templates/medium/virtual.png")));

			maskset.setSmallMulti(ImageIO.read(new File("templates/small/multi.png")));
			maskset.setSmallTrad(ImageIO.read(new File("templates/small/trad.png")));
			maskset.setSmallMyst(ImageIO.read(new File("templates/small/myst.png")));
			maskset.setSmallVirtual(ImageIO.read(new File("templates/small/virt.png")));
			maskset.setSmallEvent(ImageIO.read(new File("templates/small/event.png")));

		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	/**
	 * Load the map interface, parse data we need.
	 * 
	 * @throws IOException
	 */
	private void getUserSession() throws IOException
	{
		WebClient client = login.getClient();
		doRandomDelay();
		client.setRequestMethod("GET");
		// get the map interface
		client.goTo("http://www.geocaching.com/map/default.aspx");
		String mapUiContent = client.getContentsAsString().trim();
		String token = getContent("new Groundspeak.UserSession(", ")", mapUiContent);
		this.sessionToken = getContent("sessionToken:'", "'", token);
		this.magic = getContent("'", "'", token);
		this.subscriberType = getContent("subscriberType: ", ",", token);

	}

	/**
	 * Find all caches within a grid square.
	 * 
	 * @param loc
	 *            the centre of the search.
	 * @param zoom
	 *            the zoom level, higher zoom covers a smaller area
	 * @return an array list of caches, the caches have not been populated.
	 * @throws IOException
	 */
	public List<Cache> findCachesAtLocation(Location loc, int zoom) throws IOException
	{
		int lat = loc.getLatitudeGsJson(zoom);
		int lon = loc.getLongitudeGsJson(zoom);
		return findCachesAtGridSquare(lon - 1, lat - 1, 3, 3, zoom);
	}

	/**
	 * Search all caches within a grid square using the map.
	 * 
	 * @param lon
	 *            he horizontal grid square index.
	 * @param lat
	 *            the vertical grid square index.
	 * @param width
	 *            the width of the square to search, in number of grid squares -
	 *            must be less than 5.
	 * @param height
	 *            the height of the square to search, in number of grid squares
	 *            - must be less than 5.
	 * @param zoom
	 *            the zoom level - greater zoom, greater magnification.
	 * @return null if the search failed on the server side.
	 * @throws IOException
	 */
	public List<Cache> findCachesAtGridSquare(int lon, int lat, int width, int height,
			int zoom) throws IOException
	{
		ArrayList<Cache> res = null;
		if (sessionToken == null)
		{
			getUserSession();
		}

		List<BufferedImage> tileImages = getTileImages(lon, lat, width, height, zoom);

		boolean [] validSquares = new boolean[width*height];
		List<CacheEstimator> foundCaches = getCacheEstimates(lon, lat, width, height,
				zoom,validSquares);
		
		BufferedImage tileImage = stitchImage(tileImages,validSquares,width,height);

		if (foundCaches != null)
		{
			announceCount(foundCaches.size());
			res = new ArrayList<Cache>();

			if (tileImage != null)
			{
				ImageIO.write(tileImage, "PNG", new File("foo.png"));
				// load caches
				for (CacheEstimator ce : foundCaches)
				{
					Cache c = new Cache();
					c.setName(ce.getName());
					c.setMapRequestCode(ce.getMagicString());
					if (!ce.isOverlapping())
					{
						Rectangle rect = ce.getTileImageRect();
						int edgeThickness = getEdgeThickness(zoom);
						// for zoom < 12, only spots
						// for zoom < 14, small icons
						// otherwise, large icons
						int x1 = rect.x - edgeThickness;
						int y1 = rect.y - edgeThickness;
						int x2 = rect.x + rect.width + edgeThickness;
						int y2 = rect.y + rect.height + edgeThickness;
						x1 = x1 < 0 ? 0 : x1;
						y1 = y1 < 0 ? 0 : y1;
						x2 = x2 > tileImage.getWidth() ? tileImage.getWidth() : x2;
						y2 = y2 > tileImage.getHeight() ? tileImage.getHeight() : y2;
						BufferedImage tmp = new BufferedImage(x2 - x1, y2 - y1,
								BufferedImage.TYPE_INT_ARGB);
						Graphics2D g = tmp.createGraphics();

						g.drawImage(tileImage, 0, 0, x2 - x1, y2 - y1, x1, y1, x2, y2,
								null);

						if (zoom > 13)
						{
							// large icons
							Point location = imageToolbox.getLargeIconLocation(tmp);
							if (location != null)
							{
								tmp = imageToolbox.getLargeIcon(tmp, location);
								//all large locations are 20x20 pixels
								//add half width and height
								Point absLocation = new Point(location.x+x1+10,location.y+y1+10);
								ce.setPixelLocation(absLocation);
								c.setCacheType(imageToolbox.getType(tmp, imageToolbox
										.getMasks().getLargeEntries()));
								c.setEstimateLevel(zoom);
								c.setLocation(ce.estimateLocation());
								// check if a particular pixel is white and
								// opaque
								c.setDisabled((tmp.getRGB(2, 1) != 0xFFFFFFFF));
								res.add(c);
								this.announceCache(c);
							}
							else if (tmp.getWidth() == tmp.getHeight()
									&& tmp.getWidth() == 26)
							{
								c.setCacheType(CacheType.MULTIPLE_CACHES_AT_LOCATION);
								c.setEstimateLevel(zoom);
								c.setLocation(ce.estimateLocation());
								res.add(c);
								this.announceCache(c);
							}
						}
						else if (zoom > 11)
						{
							// medium icons
							Point location = null;
							List<MaskEntry> smallMasks = imageToolbox.getMasks()
									.getMediumEntries();
							for (MaskEntry me : smallMasks)
							{
								Point p = imageToolbox.findLocation(tmp, me.getMask());
								if (p != null)
								{
									location = p;
									location.x += me.getMask().getWidth()/2;
									location.y += me.getMask().getHeight()/2;
									c.setCacheType(me.getEntry());
									break;
								}
							}
							if (location != null)
							{
								Point absLocation = new Point(location.x+x1,location.y+y1);
								ce.setPixelLocation(absLocation);
								c.setEstimateLevel(zoom);
								c.setLocation(ce.estimateLocation());
								res.add(c);
								this.announceCache(c);
							}
							else if (tmp.getWidth() == tmp.getHeight()
									&& tmp.getWidth() == 26)
							{
								c.setCacheType(CacheType.MULTIPLE_CACHES_AT_LOCATION);
								c.setEstimateLevel(zoom);
								c.setLocation(ce.estimateLocation());
								res.add(c);
								this.announceCache(c);
							}
						}
						else
						{
							// small icons
							Point location = imageToolbox.getSmallIconLocation(tmp);
							if (location != null)
							{
								tmp = imageToolbox.getSmallIcon(tmp, location);
								//all small icons are 8x8 pixels, add half size
								Point absLocation = new Point(location.x+x1+4,location.y+y1+4);
								ce.setPixelLocation(absLocation);
								c.setCacheType(imageToolbox.getType(tmp, imageToolbox
										.getMasks().getSmallEntries()));
								c.setEstimateLevel(zoom);
								c.setLocation(ce.estimateLocation());
								res.add(c);
								this.announceCache(c);
							}
							else if (tmp.getWidth() == tmp.getHeight()
									&& tmp.getWidth() == 18)
							{
								c.setCacheType(CacheType.MULTIPLE_CACHES_AT_LOCATION);
								c.setEstimateLevel(zoom);
								c.setLocation(ce.estimateLocation());
								res.add(c);
								this.announceCache(c);
							}
						}

					}
					else
					{
						c.setCacheType(CacheType.MULTIPLE_CACHES_AT_LOCATION);
						c.setEstimateLevel(zoom);
						c.setLocation(ce.estimateLocation());
						res.add(c);
						this.announceCache(c);
					}
				}
			}
		}
		return res;
	}

	private BufferedImage stitchImage(List<BufferedImage> tileImages,
			boolean[] validSquares, int width, int height)
	{
		BufferedImage res = new BufferedImage(width*256,height*256,BufferedImage.TYPE_INT_ARGB);
		Graphics2D grph = res.createGraphics();
		int squareOffset = 0;
		for (int lonoffset = 0; lonoffset < width; lonoffset++)
		{
			for (int latoffset = 0; latoffset < height; latoffset++)
			{
				if(validSquares[squareOffset])
				{
					//if this square contains actual data, render it
					grph.drawImage(tileImages.get(squareOffset), lonoffset * 256, latoffset * 256,
							(lonoffset + 1) * 256, (latoffset + 1) * 256, 0, 0, 256, 256,
							null);
				}
				squareOffset++;
			}	
		}
		return res;
	}

	private int getEdgeThickness(int zoom)
	{
		int res = EDGE_THICKNESS_BIG;
		if (zoom < 14)
		{
			res = EDGE_THICKNESS_MID;
		}
		if (zoom < 12)
		{
			res = EDGE_THICKNESS_SML;
		}
		return res;
	}

	private List<BufferedImage> getTileImages(int lon, int lat, int width, int height, int zoom)
			throws IOException
	{
		List<BufferedImage>res = new ArrayList<BufferedImage>();
		WebClient client = login.getClient();
		for (int lonoffset = 0; lonoffset < width; lonoffset++)
		{
			for (int latoffset = 0; latoffset < height; latoffset++)
			{
				// get the image
				StringBuilder imageTileSearch = new StringBuilder(
						"http://www.geocaching.com/map/map.tile?x=");
				imageTileSearch.append(lon + lonoffset);
				imageTileSearch.append("&y=");
				imageTileSearch.append(lat + latoffset);
				imageTileSearch.append("&z=");
				imageTileSearch.append(Integer.toString(zoom));
				imageTileSearch.append("&k=");
				imageTileSearch.append(magic);
				imageTileSearch.append("&st=");
				imageTileSearch.append(sessionToken);
				// no idea what ep is, possibly subscriber type
				imageTileSearch.append("&ep=");
				imageTileSearch.append(subscriberType);
				client.setRequestMethod("GET");
				client.goTo(imageTileSearch.toString());
				// create the image
				InputStream in = new ByteArrayInputStream(client.getContents());
				BufferedImage image = ImageIO.read(in);
				// draw the image onto the composite image
				res.add(image);
			}
		}

		return res;
	}

	/**
	 * Search the map interface for a list of cache names and approximate
	 * locations in the area.
	 * 
	 * @param lon
	 * @param lat
	 * @param width
	 * @param height
	 * @param zoom
	 * @return null if the search failed on the server side.
	 * @throws IOException
	 */
	private List<CacheEstimator> getCacheEstimates(int lon, int lat, int width,
			int height, int zoom,boolean [] validSquares) throws IOException
	{
		WebClient client = login.getClient();
		List<CacheEstimator> foundCaches = null;
		int squareOffset = 0;
		for (int lonoffset = 0; lonoffset < width; lonoffset++)
		{
			for (int latoffset = 0; latoffset < height; latoffset++)
			{

				// query the search page
				StringBuilder locationSearch = new StringBuilder(
						"http://www.geocaching.com/map/map.info?x=");
				locationSearch.append(lon + lonoffset);
				locationSearch.append("&y=");
				locationSearch.append(lat + latoffset);
				locationSearch.append("&z=");
				locationSearch.append(Integer.toString(zoom));
				locationSearch.append("&k=");
				locationSearch.append(magic);
				locationSearch.append("&st=");
				locationSearch.append(sessionToken);
				// no idea what ep is, possibly subscriber type:
				locationSearch.append("&ep=");
				locationSearch.append(subscriberType);
				// this is probably a timestamp:
				locationSearch.append("&_=");
				locationSearch.append(Long.toString(System.currentTimeMillis()));
				doRandomDelay();
				client.setRequestMethod("GET");
				client.goTo(locationSearch.toString());

				String jsonContent = client.getContentsAsString().trim();

				// de-tokenise the string
				JSONTokener jtok = new JSONTokener(jsonContent);
				try
				{
					while (jtok.more())
					{
						validSquares[squareOffset] = true;
						if (foundCaches == null)
						{
							foundCaches = new ArrayList<CacheEstimator>();
						}
						Object o = jtok.nextValue();

						if (o instanceof JSONObject)
						{
							JSONObject obj = (JSONObject) o;
							JSONArray keys = (JSONArray) obj.get("keys");
							JSONObject data = (JSONObject) obj.get("data");
							for (int keyIndex = 0; keyIndex < keys.length(); keyIndex++)
							{
								String key = keys.getString(keyIndex);
								if (key.length() > 0)
								{
									JSONArray jobj = data.getJSONArray(key);
									boolean isOverlapping = jobj.length() > 1;
									for (int objectIndex = 0; objectIndex < jobj.length(); objectIndex++)
									{
										JSONObject entry = jobj
												.getJSONObject(objectIndex);
										CacheEstimator ce = new CacheEstimator(
												entry.getString("n"));
										String[] parts = key.split("_");
										int x = Integer.parseInt(parts[0]) + lonoffset
												* 64;
										int y = Integer.parseInt(parts[1]) + latoffset
												* 64;
										if (!foundCaches.contains(ce))
										{
											foundCaches.add(ce);
											ce.setMagicString(entry.getString("i"));
											ce.setGridSquare(new Point(lon,lat));
											ce.setZoomlevel(zoom);
										}
										else
										{
											ce = foundCaches.get(foundCaches.indexOf(ce));
										}
										ce.addPoint(new Point(x, y));
										if (!ce.isOverlapping())
										{
											ce.setOverlapping(isOverlapping);
										}
									}
								}
							}
						}
						else
						{
							System.out.println("Non-JSON Object:" + o);
						}
					}
				}
				catch (JSONException e)
				{
					e.printStackTrace();
				}
				squareOffset++;
			}
		}
		return foundCaches;
	}
}
